Programação Web

Aula 23 - Autenticação JWT




Helder Jefferson Ferreira da Luz

helder.luz@ifpr.edu.br

Objetivos

  • Entender o que é e como funciona o CORS.
  • Aprender sobre o padrão de autenticação JSON Web Token (JWT).
  • Implementar um fluxo de login que gera um token JWT.
  • Criar um middleware para proteger rotas usando a verificação de token.
  • Utilizar o Router do Express para organizar as rotas da aplicação.

CORS

Por padrão, o Nodejs + Express negam requisições com domínio de origem e destino diferentes.

Esse mecanismo de segurança é chamado de CORS - Cross-Origin Resource Sharing.


O servidor usa o CORS para determinar se uma solicitação deve ser permitida.

A permissão é feita pelo cabeçalho HTTP "Access-Control-Allow-Origin", que indica quais domínios têm permissão para acessar os recursos do servidor.

CORS

Para permitir, pode-se implementar um middleware que indique quais domínios tem permissão:

Um domínio de origem
app.use((req, res, next) => {
  // permite apenas o dominio especificado
  res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5501"); 
  next();
});
Permitindo todos os domínios de origem (má prática)
app.use((req, res, next) => {
  // permite todos os dominios 
  res.header("Access-Control-Allow-Origin", "*");
  next();
});

CORS

Usando o pacote CORS:

Todos os domínios de origem
import cors from 'cors';
app.use(cors(corsOptions)); 
Permitindo um domínio de origem
import cors from 'cors';
const corsOptions = {
  origin: 'http://127.0.0.1:5501',
  optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));

Autenticação JWT

JWT – JSON Web Token é um padrão aberto para transmissão segura de informações.

Utilizado para criação de Tokens compactos e autenticáveis para controlar o acesso de recursos.

Autenticação JWT

É dividido em 3 partes:

  • Header (cabeçalho): Contém informações sobre o tipo do token e o algoritmo de assinatura usado.
  • Payload (carga útil): Contém os dados que são transportados pelo token, como informações do usuário ou outras metainformações.
  • Signature (assinatura): É a parte criptograficamente assinada que verifica se o token é válido e não foi alterado.

Autenticação JWT

Signature

  • A segurança da assinatura do seu JWT depende inteiramente da força e do sigilo da sua chave secreta. Usar chaves fracas ou previsíveis (como "senha123") torna seus tokens vulneráveis a ataques.
  • A chave secreta deve ter ao menos 256bits (32 bytes).
  • Ela pode ser gerada com o comando no terminal:
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    
  • Pode ser gerada também em sites, como: https://jwtsecretkeygenerator.com

Autenticação JWT

sequenceDiagram
    Browser->>+Server: 1. POST /users/login <br> com username e password
    Note right of Server: 2. Cria um JWT com um segredo
    Server-\->>-Browser: 3. Retorna o JWT para o Browser
    Browser->>+Server: 4. Envia o JWT <br> no header de autorização
    Note right of Server: 5. Verifica a assinatura do JWT. <br> Pega a informação do usuário do JWT
    Server-\->>-Browser: 6. Envia a resposta para o cliente

Autenticação JWT

Para usar no Node.js

Instalação
npm i jsonwebtoken
Importação
import jwt from 'jsonwebtoken';

Autenticação JWT

Funções do pacote:

  • jwt.sign(payload, secretOrPrivateKey, [options, callback])
    Usado para criar um novo token JWT.

  • jwt.verify(token, secretOrPublicKey, [options, callback])
    Usado para verificar se um token JWT é válido.

JWT - login

Criação de rota para login

app.post('/login', async (req, res) => {
  let { email, senha } = req.body;

  // IMPORTANTE: Exemplo simplificado, nunca compare senhas em texto plano.
  if (email != 'teste@gmail.com' || senha !== '123456') {
    return res.status(401).send({auth: false, 
      mensagem: 'Usuário ou senha inválidos'})
  }

  let token = jwt.sign(
    {email: email}, 
    process.env.JWT_SECRET, 
    {expiresIn: process.env.JWT_EXPIRES_IN}
  );
  res.status(200).send({auth: true, token: token})
})

JWT - verificação do token

Criação de middleware para autenticar o token

function autenticarToken(req, res, next) {
  const { authorization } = req.headers;

  if (!authorization) 
    return res.status(401).send({auth: false, 
      mensagem: 'Nenhum token informado'})

  const token = authorization.split(' ')[1]; // Bearer token
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.email = decoded.email; // Adiciona o payload decodificado à requisição
    next(); // Passa para a próxima rota/middleware
  } catch (err) {
    return res.status(401).send({ auth: false, 
      mensagem: 'Token inválido ou expirado.' });
  }
}

JWT - autenticar rota

Usando o middleware de autenticação implementado em uma das rotas da aplicação

app.get('/clientes', autenticarToken, async (req, res) => {
  res.status(200).send(await database.getClientes())
})

Para testar a rota com o REST Client

@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImhlbGRlciIsImlhdCI6MTc2MzUyMjE3MCwiZXhwIjoxNzY0MTI2OTcwfQ.bhctO_L6UVh2XHMIYoJ-Z7gaFeiFLHLARBlXo1REIMM

###
GET http://localhost:3000/clientes
Authorization: Bearer {{token}}

Armazenamento de senha

Ao armazenar a senha no banco de dados, é importante não deixa-la bruta, armazenando ela embaralhada.

Para isso há o hash.
O pacote recomendado para aplicar hash na senha é o:

Instalação do pacote
npm i argon2

Armazenamento de senha

Cadastro
import argon2 from 'argon2';

// Método POST
const { email, password } = req.body;
const hashedPassword = await argon2.hash(password);

const id = await createUser(email, hashedPassword);
Login
import argon2 from 'argon2';

// Método POST
let { email, password } = req.body;
const user = await getUser(email);

const validPassword = await argon2.verify(user.password, password);

Armazenamento e envio do token

O token não deve ser armazenamento de forma bruta no localStorage, pois qualquer código JS que executar no seu domínio terá acesso ao token.


Idealmente, deve ser utilizado o Cookies para armazena-lo, sendo necessário utilizar um novo pacote no node.js.

Instalação
npm i cookie-parser
Importação
import cookieParser from 'cookie-parser';

Armazenamento e envio do token

Uso do middleware para cookies
app.use(cookieParser());

Na mensagem de envio do token, é necessário configurar o cookie.

  res.cookie('authToken', token, {
    httpOnly: true, // Previne JavaScript client-side de acessar o cookie
    maxAge: 24 * 3600000, // Expiração do cookie em milissegundos (1 dia)
    sameSite: 'Lax' // Protege contra ataques CSRF
  });

Armazenamento do token

app.post('/login', async (req, res) => {
  let { email, senha } = req.body;

  if (email != 'teste@gmail.com' || senha !== '123456') {
    return res.status(401).send({
      auth: false, mensagem: 'Usuário ou senha inválidos'
    })
  }

  let token = jwt.sign({ email: email }, process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN });

  res.cookie('authToken', token, {
    httpOnly: true, maxAge: 24 * 3600000, sameSite: 'Lax'
  });
  res.status(200).send({ auth: true, message: 'Login realizado com sucesso' })
})

JWT - requisição no cliente

Para o cliente requisitar um recurso com autenticação JWT, o cookie é automaticamente enviado com a requisição.

async function recuperarClientes() {
  let header = {
    method: 'POST'
  }

  let resposta = await fetch('/clientes', header)
  let clientes = await resposta.json()
  console.log(clientes)
}

Gerenciamento de rotas

À medida que uma aplicação cresce, manter todas as rotas no arquivo principal (server.js) se torna insustentável. O código fica longo, desorganizado e difícil de manter.


O express.Router é a solução para isso. Ele funciona como uma "mini-aplicação" que permite agrupar rotas relacionadas a um mesmo recurso (como clientes e produtos) em seus próprios arquivos.

Gerenciamento de rotas

As rotas são separadas do arquivo principal, sendo criado um arquivo para cada recurso da sua API.


Exemplo:

📁 API/
├───📁 routes/
│   ├── clientes.routes.js
│   ├── pedidos.routes.js
│   └── produtos.routes.js
└── server.js

Gerenciamento de rotas

No arquivo de rotas (ex: routes/cliente.routes.js):

  • Cria uma instância do Router.
  • Define as rotas diretamente nela (router.get, router.post).
  • Exporta o router para ser usado na aplicação principal.

Gerenciamento de rotas

Router do express

import { Router } from 'express';
import { getClientes, getClientById, createClient } from '../database.js';
const router = Router();

router.get('/', async (req, res) => {
  const clientes = await getClientes();
  res.status(200).json(clientes);
})

router.get('/:id', async (req, res) => {
  const id = req.params.id;
  const cliente = await getClientById(id);
  res.status(200).json(cliente);
})

export default router;

Gerenciamento de rotas

As rotas são importadas no arquivo principal server.js.

Importação de uso das rotas
import clientRoutes from './routes/cliente.routes.js';

app.use('/clientes', autenticarToken, clientRoutes);

Prefixo de Rota: Todas as rotas definidas em clientRoutes serão acessadas a partir de /clientes. Por exemplo, o router.get('/:id', ...) se torna acessível em GET /clientes/:id.
Middleware de Grupo: Com o middleware autenticarToken, todas as rotas dentro de clientRoutes passem por essa autenticação.

Dúvidas? 🤔

Exercícios

  1. Crie uma nova rota GET /perfil que retorne o e-mail do usuário logado (obtido a partir do token). Proteja esta rota com o middleware autenticarToken.
  2. Implemente a rota POST /usuarios/cadastrar que recebe email e senha, gera o hash da senha usando argon2 e salva o novo usuário (pode ser em um array em memória, para simplificar). No banco de dados, crie uma tabela para usuarios, que armazene o email e a senha para fins de autenticação.
  3. Modifique a rota POST /login para buscar o usuário cadastrado e comparar a senha fornecida com o hash salvo, usando argon2.verify.
  4. Teste o sistema com um tempo de expiração de token bem curto (ex: 10s) e veja o que acontece ao tentar acessar uma rota protegida após o tempo expirar.

--- # Manipulação de arquivos